﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Text;
using System.Transactions;

using Ordinaire;
using Ordinaire.Commerce;
using Ordinaire.Commerce.Data;
using Ordinaire.Commerce.Purchasing;
using Ordinaire.Commerce.Purchasing.Data;
using Ordinaire.Commerce.Purchasing.Db;
using Ordinaire.Social.Data;
using Ordinaire.Taxonomy;

namespace Ordinaire.Commerce.Purchasing
{
    /// <summary>
    /// Represents the custodian that manages all purchases.
    /// </summary>
    public class PurchasingManager : BusinessManager<PurchasingDataManager, PContext>, IPurchasing
    {
        #region Variables

        /// <summary>
        /// Ordinaire.Commerce.GoodCatalogue instance holder.
        /// </summary>
        private GoodCatalog catalogue;

        /// <summary>
        /// Ordinaire.Taxonomy.Vocabularies instance holder.
        /// </summary>
        private Vocabularies vocabularies;

        /// <summary>
        /// Reference of vendor relation manager.
        /// </summary>
        [Import(typeof(IVendorRelation), AllowDefault = true)]
        public Lazy<IVendorRelation> vendorRelationManager;

        public CommerceManager<PContext> commerceManager;

        [ImportMany(typeof(PurchasingObserver))]
        public IEnumerable<PurchasingObserver> observers;

        #endregion

        #region Constructor

        /// <summary>
        /// Creates new instance of Ordinaire.Commerce.Purchasing.PurchasingManager.
        /// </summary>
        public PurchasingManager()
        {
            base.dataManager = new PurchasingDataManager();

            this.vocabularies = new Vocabularies();
            this.ShareContext(vocabularies);

            this.catalogue = new GoodCatalog();
            this.ShareContext(catalogue);

            this.commerceManager = new CommerceManager<PContext>();
            this.ShareContext(commerceManager);

            ExtensionContainer.Instance.ComposeParts(this);
        }

        #endregion

        #region Public methods

        #region IPurchasing overrides

        /// <summary>
        /// Returns vendor id (personId or organisationId) of specified purchase id.
        /// </summary>
        /// <param name="purchaseId">purchase id</param>
        /// <param name="personId">person id as returned value, if set</param>
        /// <param name="organisationId">organisation id as returned value, if set</param>
        public void GetVendorIdByPurchaseId(int purchaseId, out int? personId, out int? organisationId)
        {
            purchaseId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchaseId '{0}'", purchaseId)));

            personId       = null;
            organisationId = null;

            var data = dataManager.Get(purchaseId);
            if (data != null)
            {
                personId       = data.PersonId;
                organisationId = data.OrganisationId;
            }
        }

        #endregion

        /// <summary>
        /// Adds new purchase item into the specified purchase id.
        /// </summary>
        /// <param name="purchaseId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total purchase value</param>
        /// <param name="measureUnitId">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <returns>the reference id of the new purchase item</returns>
        public int AddPurchaseItem(int purchaseId, int goodId, int quantity, decimal value, int measureUnitId = 0, decimal tax = 0, decimal discount = 0)
        {
            purchaseId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", purchaseId)));
            goodId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("good id '{0}'", goodId)));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            // Notify observers
            var args = new { purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnPreAddPurchaseItem", this, args);

            int purchaseGoodId = this.commerceManager.AddInvoiceItem<PurchaseItems, Purchases>(purchaseId, goodId, quantity, value, measureUnitId, tax, discount);

            // Notify observers
            args = new { purchaseGoodId, purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnAddPurchaseItem", this, args);

            return purchaseGoodId;
        }

        /// <summary>
        /// Adds new purchase item into the specified purchase id.
        /// </summary>
        /// <param name="purchaseId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total purchase value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <returns>the reference id of the new purchase item</returns>
        public int AddPurchaseItem(int purchaseId, string goodCode, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0)
        {
            purchaseId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", purchaseId)));
            goodCode.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "goodCode"));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            Good good = catalogue.GetByCode(goodCode);
            good.ThrowIfNull<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("code '{0}'", goodCode)));

            int goodId = good.Id;
            int measureUnitId = measureUnit.ToVocabId<MeasureUnits>();

            // Notify observers
            var args = new { purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnPreAddPurchaseItem", this, args);

            int purchaseGoodId = this.commerceManager.AddInvoiceItem<PurchaseItems, Purchases>(purchaseId, good.Id, quantity, value, measureUnit.ToVocabId<MeasureUnits>(), tax, discount);

            // Notify observers
            args = new { purchaseGoodId, purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnAddPurchaseItem", this, args);

            return purchaseGoodId;
        }

        /// <summary>
        /// Adds new purchase item into the specified purchase id.
        /// </summary>
        /// <param name="purchaseId">the purchase id where the item is added to</param>
        /// <param name="goodCode">the good code</param>
        /// <param name="goodName">the good name</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total purchase value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <returns>the reference id of the new purchase item</returns>
        public int AddPurchaseItem(int purchaseId, string goodCode, string goodName, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0)
        {
            purchaseId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", purchaseId)));
            goodName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "goodName"));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            // Retrieve or add good into catalog
            int goodId = goodCode.ToGoodIdByCode(goodName, measureUnit, CommerceCalculator.ValuePerUnit(value, quantity));
            int measureUnitId = measureUnit.ToVocabId<MeasureUnits>();

            // Notify observers
            var args = new { purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnPreAddPurchaseItem", this, args);

            // Add good into purchase
            int purchaseGoodId = this.commerceManager.AddInvoiceItem<PurchaseItems, Purchases>(purchaseId, goodId, quantity, value, measureUnit.ToVocabId<MeasureUnits>(), tax, discount);

            // Notify observers
            args = new { purchaseGoodId, purchaseId, goodId, quantity, value, measureUnitId, tax, discount }.ToDictionary();
            observers.InvokeEach("OnAddPurchaseItem", this, args);

            return purchaseGoodId;
        }

        /// <summary>
        /// Add new purchase payment item into the specified purchase id.
        /// </summary>
        /// <param name="purchaseId">the purchase id where the item is added to</param>
        /// <param name="value">the payment value</param>
        /// <param name="method">the payment method</param>
        /// <param name="date">the payment date</param>
        /// <param name="purchaseItemId">the associated purchase good id</param>
        /// <returns>the reference id of the new purchase payment item</returns>
        //public int AddPayment(int purchaseId, decimal value, string method = null, DateTime? date = null, int? purchaseItemId = null)
        //{
        //    purchaseId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", purchaseId)));
        //    value.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", purchaseId)));

        //    int purchasePaymentId = HelpAddPayment(purchaseId, value, method, date);
        //    return purchasePaymentId;
        //}

        /// <summary>
        /// Deletes purchase item with specified record id.
        /// </summary>
        /// <param name="purchaseItemId">the PurchaseGoods id</param>
        /// <returns>true if delete is successful; otherwise false</returns>
        public bool DeletePurchaseItem(int purchaseItemId)
        {
            purchaseItemId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase item id '{0}'", purchaseItemId)));

            // Notify observers
            var args = new { purchaseGoodId=purchaseItemId }.ToDictionary();
            observers.InvokeEach("OnPreDeletePurchaseItem", this, args);

            bool @return = this.commerceManager.DeleteInvoiceItem<PurchaseItems, Purchases>(purchaseItemId);

            // Notify observers
            args = new { purchaseGoodId = purchaseItemId, @return }.ToDictionary();
            observers.InvokeEach("OnDeletePurchaseItem", this, args);

            return @return;
        }

        /// <summary>
        /// Returns invoice with the specified id.
        /// </summary>
        /// <param name="id">invoice id</param>
        /// <returns>invoice with the specified id</returns>
        public Purchase GetPurchase(int id)
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", id)));

            Purchases purchaseData = dataManager.Get(id);
            if (purchaseData == null)
            {
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", id));
            }

            return purchaseData.ToPurchase();
        }

        /// <summary>
        /// Creates new invoice number in the data source.
        /// </summary>
        /// <param name="date">the invoice date</param>
        /// <param name="organisationId">the organisation id where the goods was purchased from</param>
        /// <param name="personId">the person id whom the goods was purchased from</param>
        /// <param name="serialNo">optional purchase serial number</param>
        /// <returns>the reference id of the new invoice</returns>
        public int NewPurchase(DateTime? date = null, int organisationId = 0, int personId = 0, string serialNo = "")
        {
            // Notify observers
            var args = new { date, organisationId, personId, serialNo }.ToDictionary();
            observers.InvokeEach("OnPreNewPurchase", this, args);

            int purchaseId = this.commerceManager.NewInvoice<Purchases>(date, organisationId, personId, serialNo);

            // Notify observers
            args = new { purchaseId, date, organisationId, personId, serialNo }.ToDictionary();
            observers.InvokeEach("OnNewPurchase", this, args);

            return purchaseId;
        }

        #endregion

        #region Protected methods

        /// <summary>
        /// Returns new invoice number in "IyyMMddhhmmssf" format (eg. INV1303111141301)
        /// </summary>
        /// <returns>the invoice number</returns>
        protected virtual string GenerateSerialNo()
        {
            return DateTime.Now.ToString("IyyMMddHHmmssf");
        }

        /// <summary>
        /// Serves as a helper function to add new purchase payment item into the specified purchase id.
        /// </summary>
        /// <param name="purchaseId">the purchase id where the purchase item is stored to</param>
        /// <param name="value">the payment amount</param>
        /// <param name="method">the payment method</param>
        /// <param name="date">the payment desired</param>
        /// <param name="purchaseItemId">the associated purchase good id</param>
        /// <returns>the reference id of the payment item</returns>
        //protected int HelpAddPayment(int purchaseId, decimal value, string method = null, DateTime? date = null, int? purchaseItemId = null)
        //{
        //    // Notify observers
        //    var args = new { purchaseId, value, method, date }.ToDictionary();
        //    observers.InvokeEach("OnPreAddPayment", this, args);

        //    InvoiceItems newData = new InvoiceItems {
        //        ForeignId       = purchaseId,
        //        GoodId          = 0,
        //        PurchaseItemId  = purchaseItemId,
        //        Quantity        = 1,
        //        Value           = value * -1,
        //        //Method        = method,
        //        Date            = (date.HasValue) ? date.Value : DateTime.Today.Date,
        //    };
        //    dataManager.Insert<InvoiceItems, int, string>(newData);

        //    // Notify observers
        //    args = new { purchasePaymentId = newData.Id, purchaseId, value, method, date }.ToDictionary();
        //    observers.InvokeEach("OnPreAddPayment", this, args);

        //    return newData.Id;
        //}

        #endregion
    }
}
